from neuronThread import *
from neuronConnexion import *
import random
import xml.dom.minidom

class NeuralNetwork:
    def __init__(self):
        self.neuronsList=[]
        self.config="No config"
        self.connexionsList=[]

    def addNeuron(self,neuron,id=None):
        """Adds a neuron to the network, unconnected. Optionnal argument is the id string.
The method does not check yet if the id is already taken, better to leave default value"""
        self.neuronsList.append(neuron)
        if id==None:
            id=len(self.neuronsList)
            neuron.setId(id)
        else:
            neuron.setId(id)

    def getNeuronById(self,id):
        """Returns the neuron corresponding to the given id. If not found returns None"""
        for neuron in self.neuronsList:
            if neuron.getId()==id:
                return neuron
        print("Neuron not found for id=%s" %(id))
        return None
    def detectExtremeNeurons(self):
        """This functions detects sensor neurons and output neurons
                it is called regularly as the network evolves as it goes"""
        self.outputNeurons=[]
        self.inputNeurons=[]
        for neuron in self.neuronsList:
            if not neuron.hasParents():
                self.inputNeurons.append(neuron)
            if not neuron.hasChildren():
                self.outputNeurons.append(neuron)

    def __str__(self):
        string=""
        for neuron in self.neuronsList:
            string+=str(neuron)
        return string

    def randomInput(self):
        """Sends a random input into the network.
This is a good idea to test the structure of the network"""
        result=[]
        self.detectExtremeNeurons()
        for neuron in self.inputNeurons:
            neuron.computeSensorOutput(random.randint(-100,100)/100)
        for neuron in self.outputNeurons:
            result.append(neuron.getResult())
        return result

    def randomizeCoefficients(self,mu=25,sigma=0,method=random.gauss):
        """Randomize coefficients with gaussian by default.
Usage in the fonction is method(mu,sigma) so can be modified by using for example
neuralNetwork.randomize(-100,100,random.randint)"""
        for conn in self.connexionsList:
            conn.setCoefficient(method(mu,sigma))

    def connectNeurons(self,n1,n2,coeff=1):
        """n1 is the parent neuron, n2 the child"""
        nc=NeuronConnexion(n1,n2)
        nc.setCoefficient(coeff)
        self.connexionsList.append(nc)

    def setConfig(self,string,nbNeurons):
        """Sets the network into a classical configuration
            valid configurations:
                tree(default)
                chain

            second argument is the number of neurons if improper number, it will be changed to closest available"""
        self.neuronsList=[]
        self.config=string
        if string=="chain":
            self.setConfigChain(nbNeurons)
        else:
            self.setConfigTree(nbNeurons)
        #self.randomizeCoefficients()

    def getConfig(self):
        return self.config

    def setConfigChain(self,nbNeurons):
        """This sets the neural network to a chain
            Should not be accessed directly, appropriated method is setConfig("chain",10)"""
        prevNeuron=Neuron()
        self.addNeuron(prevNeuron)
        for i in range(nbNeurons-1):
            currNeuron=Neuron()
            self.addNeuron(currNeuron)
            self.connectNeurons(prevNeuron,currNeuron)
            prevNeuron=currNeuron

    def setConfigTree(self,nbNeurons):
        """This sets the neural network to a tree
Should not be accessed directly, appropriated method is setConfig("tree",10)"""
        n=0
        while( 2**(n+1)-1<nbNeurons):
            n+=1
        neuronsLeft=2**(n+1)-1
        if nbNeurons!=neuronsLeft:
            print("Warning : %i neurons is not valid for a tree structure using %i neurons instead"\
                  %(nbNeurons,neuronsLeft))
        self.addNeuron(Neuron())
        neuronsLeft-=1
        while neuronsLeft>0:
            self.detectExtremeNeurons()
            self.addParents(self.inputNeurons,2)
            neuronsLeft-=2*len(self.inputNeurons)

    def addParents(self,neuronsList,k):
        """This adds k parents to given neurons in the list argument,
only used by setConfigTree"""
        for n in neuronsList:
            p=None
            for i in range(k):
                p=Neuron()
                self.addNeuron(p)
                self.connectNeurons(p,n)

    def save(self,file="brain.xml"):
        """This saves the network into a file. Default is "brain.xml" """
        doc=xml.dom.minidom.Document()
        neuralNetwork=doc.createElement("NeuralNetwork")
        neuralNetwork.setAttribute("config",self.config)
        doc.appendChild(neuralNetwork)
        for neur in self.neuronsList:
            neuron=doc.createElement("Neuron")
            neuronThresholdMethod=doc.createElement("NeuronThresholdMethod")
            neuronThresholdMethod.appendChild(doc.createTextNode(\
                str(neur.getThresholdMethod())))
            neuron.appendChild(neuronThresholdMethod)
            neuronId=doc.createElement("NeuronId")
            neuronId.appendChild(doc.createTextNode(str(neur.getId())))
            neuron.appendChild(neuronId)
            neuralNetwork.appendChild(neuron)
        for conn in self.connexionsList:
            neuronConnexion=doc.createElement("NeuronConnexion")
            parent=doc.createElement("Parent")
            child=doc.createElement("Child")
            coeff=doc.createElement("Coefficient")
            parent.appendChild(doc.createTextNode(str(conn.getParentNeuron().getId())))
            child.appendChild(doc.createTextNode(str(conn.getChildNeuron().getId())))
            coeff.appendChild(doc.createTextNode(str(conn.getCoefficient())))
            neuronConnexion.appendChild(parent)
            neuronConnexion.appendChild(child)
            neuronConnexion.appendChild(coeff)
            neuralNetwork.appendChild(neuronConnexion)
        f=open(file,"w")
        f.write(doc.toprettyxml())
        f.close()

    def load(self,file="brain.xml"):
        """Loads a xml file to set up the network, default file is "brain.xml" """
        self.neuronsList=[]
        self.connexionsList=[]
        self.config="No Config"
        doc=xml.dom.minidom.parse(file)
        neuralNetwork=doc.getElementsByTagName("NeuralNetwork")[0]
        if neuralNetwork.hasAttribute("config"):
            self.config=neuralNetwork.getAttribute("config")
        conns=neuralNetwork.getElementsByTagName("NeuronConnexion")
        neurons=neuralNetwork.getElementsByTagName("Neuron")
        for xmlneuron in neurons:
            id=xmlneuron.getElementsByTagName("NeuronId")[0]
            thresholdMethod=xmlneuron.getElementsByTagName("NeuronThresholdMethod")[0]
            neuron=Neuron()
            neuron.setThresholdMethod(thresholdMethod.firstChild.data.strip())
            self.addNeuron(neuron,id.firstChild.data.strip())
        for xmlconn in conns:
            parentId=xmlconn.getElementsByTagName("Parent")[0].firstChild.data.strip()
            childId=xmlconn.getElementsByTagName("Child")[0].firstChild.data.strip()
            coeff=int(xmlconn.getElementsByTagName("Coefficient")[0].firstChild.data.strip())
            par=self.getNeuronById(parentId)
            child=self.getNeuronById(parentId)
            if par and child:
                self.connectNeurons(par,child,coeff)
            else:
                print("Erreur les ids %i et %i sont introuvables" %(parentId,childId))

    def __str__(self):
        string="config:%s \nnumber of neurons: %i\nnumber of connexions: %i"\
                %(self.config,len(self.neuronsList),len(self.connexionsList))
##        for neuron in self.neuronsList:
##            string+=str(neuron)+"\n"
        return string

